基於springboot的web專案最佳實踐
springboot
可以說是現在做javaweb
開發最火的技術,我在基於springboot
搭建專案的過程中,踩過不少坑,發現整合框架時並非僅僅引入starter
那麼簡單。
要做到簡單,易用,擴充套件性更好,還需做不少二次封裝,於是便寫了個基於springboot
的web專案腳手架,對一些常用的框架進行整合,並進行了簡單的二次封裝。
專案名baymax
取自動畫片超能陸戰隊裡面的大白,大白是一個醫護充氣機器人,希望這個專案你能像大白一樣貼心,可以減少你的工作量。
github https://github.com/zhaoguhong/baymax
web
web模組是開發web專案必不可少的一個模組
maven 依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
對於前後端分離專案,推薦直接使用@RestController
註解
需要注意的是,不建議直接用RequstMapping註解並且不指定方法型別的寫法,推薦使用GetMaping
或者PostMaping
之類的註解
@SpringBootApplication @RestController public class BaymaxApplication { public static void main(String[] args) { SpringApplication.run(BaymaxApplication.class, args); } @GetMapping("/test") public String test() { return "hello baymax"; } }
單元測試
spring 對單元測試也提供了很好的支援
maven 依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
新增 @RunWith(SpringRunner.class)
和 @SpringBootTest
即可進行測試
@RunWith(SpringRunner.class)
@SpringBootTest
public class WebTest {
}
對於Controller
層的介面,可以直接用MockMvc
進行測試
@RunWith(SpringRunner.class)
@SpringBootTest
public class WebTest {
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@Before
public void setUp() throws Exception {
mvc = MockMvcBuilders.webAppContextSetup(context).build();
}
@Test
public void testValidation() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/test"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.content().string("hello baymax"));
}
}
actuator應用監控
actuator 是 spring 提供的應用監控功能,常用的配置項如下
# actuator埠 預設應用埠
management.server.port=8082
# 載入所有的端點 預設只加載 info,health
management.endpoints.web.exposure.include=*
# actuator路徑字首,預設 /actuator
management.endpoints.web.base-path=/actuator
lombok
lombok可以在編譯期生成對應的java程式碼,使程式碼看起來更簡潔,同時減少開發工作量
用lombok後的實體類
@Data
public class Demo {
private Long id;
private String userName;
private Integer age;
}
需要注意,@Data
包含 @ToString、@Getter、@Setter、@EqualsAndHashCode、@RequiredArgsConstructor
,RequiredArgsConstructor 並不是無參構造,無參構造的註解是NoArgsConstructor
RequiredArgsConstructor
會生成 會生成一個包含常量(final),和標識了@NotNull的變數 的構造方法
baseEntity
把表中的基礎欄位抽離出來一個BaseEntity,所有的實體類都繼承該類
/**
* 實體類基礎類
*/
@Data
public abstract class BaseEntity implements Serializable {
/**
* 主鍵id
*/
private Long id;
/**
* 建立人
*/
private Long createdBy;
/**
* 建立時間
*/
private Date createdTime;
/**
* 更新人
*/
private Long updatedBy;
/**
* 更新時間
*/
private Date updatedTime;
/**
* 是否刪除
*/
private Integer isDeleted;
}
統一響應返回值
前後端分離專案基本上都是ajax呼叫,所以封裝一個統一的返回物件有利於前端統一處理
/**
* 用於 ajax 請求的響應工具類
*/
@Data
public class ResponseResult<T> {
// 未登入
public static final String UN_LOGIN_CODE = "401";
// 操作失敗
public static final String ERROR_CODE = "400";
// 伺服器內部執行錯誤
public static final String UNKNOWN_ERROR_CODE = "500";
// 操作成功
public static final String SUCCESS_CODE = "200";
// 響應資訊
private String msg;
// 響應code
private String code;
// 操作成功,響應資料
private T data;
public ResponseResult(String code, String msg, T data) {
this.msg = msg;
this.code = code;
this.data = data;
}
}
返回給前端的值用ResponseResult
包裝一下
/**
* 測試成功的 ResponseResult
*/
@GetMapping("/successResult")
public ResponseResult<List<Demo>> test() {
List<Demo> demos = demoMapper.getDemos();
return ResponseResult.success(demos);
}
/**
* 測試失敗的 ResponseResult
*/
@GetMapping("/errorResult")
public ResponseResult<List<Demo>> demo() {
return ResponseResult.error("操作失敗");
}
ResponseEntity
spring其實封裝了ResponseEntity 處理響應,ResponseEntity 包含 狀態碼,頭部資訊,響應體 三部分
/**
* 測試請求成功
* @return
*/
@GetMapping("/responseEntity")
public ResponseEntity<String> responseEntity() {
return ResponseEntity.ok("請求成功");
}
/**
* 測試伺服器內部錯誤
* @return
*/
@GetMapping("/InternalServerError")
public ResponseEntity<String> responseEntityerror() {
return new ResponseEntity<>("出錯了", HttpStatus.INTERNAL_SERVER_ERROR);
}
異常
自定義異常體系
為了方便異常處理,定義一套異常體系,BaymaxException 做為所有自定義異常的父類
// 專案所有自定義異常的父類
public class BaymaxException extends RuntimeException
// 業務異常 該異常的資訊會返回給使用者
public class BusinessException extends BaymaxException
// 使用者未登入異常
public class NoneLoginException extends BaymaxException
全域性異常處理
對所有的異常處理後再返回給前端
@RestControllerAdvice
public class GlobalControllerExceptionHandler {
/**
* 業務異常
*/
@ExceptionHandler(value = {BusinessException.class})
public ResponseResult<?> handleBusinessException(BusinessException ex) {
String msg = ex.getMessage();
if (StringUtils.isBlank(msg)) {
msg = "操作失敗";
}
return ResponseResult.error(msg);
}
/**
* 處理未登入異常
*/
@ExceptionHandler(value = {NoneLoginException.class})
public ResponseResult<?> handleNoneLoginException(NoneLoginException ex) {
return ResponseResult.unLogin();
}
異常持久化
對於未知的異常,儲存到資料庫,方便後續排錯
需要說明是的,如果專案訪問量比較大,推薦用 ELK 這種成熟的日誌分析系統,不推薦日誌儲存到關係型資料庫
@Autowired
private ExceptionLogMapper exceptionLogMapper;
/**
* 處理未知的錯誤
*/
@ExceptionHandler(value = {Exception.class})
public ResponseResult<Long> handleunknownException(Exception ex) {
ExceptionLog log = new ExceptionLog(new Date(), ExceptionUtils.getStackTrace(ex));
exceptionLogMapper.insert(log);
ResponseResult<Long> result = ResponseResult.unknownError("伺服器異常:" + log.getId());
result.setData(log.getId());
return result;
}
異常日誌介面
對外開一個異常日誌查詢介面/anon/exception/{異常日誌id}
,方便查詢
@RestController
@RequestMapping("/anon/exception")
public class ExceptionController {
@Autowired
private ExceptionLogMapper exceptionLogMapper;
@GetMapping(value = "/{id}")
public String getDemo(@PathVariable(value = "id") Long id) {
return exceptionLogMapper.selectByPrimaryKey(id).getException();
}
}
資料校驗
JSR 303定義了一系列的 Bean Validation 規範,Hibernate Validator 是 Bean Validation 的實現,並進行了擴充套件
spring boot 使用 也非常方便
public class Demo extends BaseEntity{
@NotBlank(message = "使用者名稱不允許為空")
private String userName;
@NotBlank
private String title;
@NotNull
private Integer age;
}
引數前面新增@Valid註解即可
@PostMapping("/add")
public ResponseResult<String> add(@RequestBody @Valid Demo demo) {
demoMapper.insert(demo);
return ResponseResult.success();
}
對於校驗結果可以每個方法單獨處理,如果不處理,會丟擲有異常,可以對校驗的異常做全域性處理
在 GlobalControllerExceptionHandler
新增
/**
* 處理校驗異常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseResult<?> handleValidationException(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
if (result.hasErrors()) {
StringJoiner joiner = new StringJoiner(",");
List<ObjectError> errors = result.getAllErrors();
errors.forEach(error -> {
FieldError fieldError = (FieldError) error;
joiner.add(fieldError.getField() + " " + error.getDefaultMessage());
});
return ResponseResult.error(joiner.toString());
} else {
return ResponseResult.error("操作失敗");
}
}
log
spring boot 的預設使用的日誌是logback
,web模組依賴的有日誌 starter
,所以這裡不用再引入依賴,詳細配置
修改日誌級別
Actuator 元件提供了日誌相關介面,可以查詢日誌級別或者動態修改日誌級別
// 檢視所有包/類的日誌級別
/actuator/loggers
// 檢視指定包/類日誌級別 get 請求
/actuator/loggers/com.zhaoguhong.baymax.demo.controller.DemoController
//修改日誌級別 post 請求 引數 {"configuredLevel":"debug"}
/actuator/loggers/com.zhaoguhong.baymax.demo.controller.DemoController
日誌切面
新增一個日誌切面,方便記錄方法執行的入參和出參
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogAspect {
/**
* 日誌描述
*/
String value() default "";
/**
* 日誌級別
*/
String level() default "INFO";
}
使用時直接新增到方法上即可
swagger
swagger 是一個很好用的文件生成工具
maven 依賴
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
相關配置
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Value("${swagger.enable:false}")
private boolean swaggerEnable;
//文件訪問字首
public static final String ACCESS_PREFIX = "/swagger-resources/**,/swagger-ui.html**,/webjars/**,/v2/**";
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
// 設定是否開啟swagger,生產環境關閉
.enable(swaggerEnable)
.select()
// 當前包路徑
.apis(RequestHandlerSelectors.basePackage("com.zhaoguhong.baymax"))
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.any())
.build();
}
// 構建api文件的詳細資訊
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
// 頁面標題
.title("介面文件")
// 建立人
.contact(new Contact("孤鴻", "https://github.com/zhaoguhong/baymax", ""))
// 版本號
.version("1.0")
// 描述
.description("大白的介面文件")
.build();
}
}
為文件加一個開關
#是否開啟swagger文件,生產環境關閉
swagger.enable=true
然後就可以愉快的在寫程式碼的同時寫文件了
@PostMapping("/add")
@ApiOperation(value = "新增 demo")
public ResponseResult<String> add(@RequestBody @Valid Demo demo) {
demoMapper.insert(demo);
return ResponseResult.success();
}
@ApiModel("示例")
public class Demo extends BaseEntity{
@ApiModelProperty("使用者名稱")
private String userName;
@ApiModelProperty("標題")
private String title;
@ApiModelProperty("年齡")
private Integer age;
}
訪問 localhost:8080/swagger-ui.html
就可以看到效果了
資料庫連線池
springboot1.X的資料庫連線池是tomcat連線池,springboot2預設的資料庫連線池由Tomcat換成 HikariCP,HikariCP
是一個高效能的JDBC連線池,號稱最快的連線池
Druid 是阿里巴巴資料庫事業部出品,為監控而生的資料庫連線池,這裡選取Druid
作為專案的資料庫連線池
maven 依賴
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
設定使用者名稱密碼
spring.datasource.druid.stat-view-servlet.login-username=admin
spring.datasource.druid.stat-view-servlet.login-password=123456
然後就可以訪問 localhost:8080/druid
看監控資訊了
spring jdbc
maven 依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
spring對jdbc做了封裝和抽象,最常用的是 jdbcTemplate
和 NamedParameterJdbcTemplate
兩個類,前者使用佔位符,後者使用命名引數,我在jdbcDao
做了一層簡單的封裝,提供統一的對外介面
jdbcDao
主要方法如下:
// 佔位符
find(String sql, Object... args)
// 佔位符,手動指定對映mapper
find(String sql, Object[] args, RowMapper<T> rowMapper)
// 命名引數
find(String sql, Map<String, ?> paramMap)
// 命名引數,手動指定對映mapper
find(String sql, Map<String, ?> paramMap, RowMapper<T> rowMapper)
//springjdbc 原queryForMap方法,如果沒查詢到會拋異常,此處如果沒有查詢到,返回null
queryForMap(String sql, Object... args)
queryForMap(String sql, Map<String, ?> paramMap)
// 分頁查詢
find(Page<T> page, String sql, Map<String, ?> parameters, RowMapper<?> mapper)
// 分頁查詢
find(Page<T> page, String sql, RowMapper<T> mapper, Object... args)
jpa
jpa
是 java
持久化的標準,spring data jpa
使操作資料庫變得更方便,需要說明的 spring data jpa
本身並不是jpa的實現,它預設使用的 provider
是 hibernate
maven 依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
我把通用的方法抽取了出來了,封裝了一個BaseRepository,使用時,直接繼承該介面即可
public interface DemoRepository extends BaseRepository<Demo> {
}
BaseRepository
主要方法如下
// 新增,會對建立時間,建立人自動賦值
void saveEntity(T entity)
// 更新,會對更新時間,更新人自動賦值
void updateEntity(T entity)
// 邏輯刪除
void deleteEntity(T entity)
// 批量儲存
void saveEntites(Collection<T> entitys)
// 批量更新
void updateEntites(Collection<T> entitys)
// 批量邏輯刪除
void deleteEntites(Collection<T> entitys)
// 根據id獲取實體,會過濾掉邏輯刪除的
T getById(Long id)
如果想使用傳統的sql形式,可以直接使用JpaDao,為了方便使用,我儘量使JpaDao和JdbcDao的介面保持統一
JpaDao
主要方法如下
// 佔位符 例如:from Demo where id =?
find(String sql, Object... args)
// 命名引數
find(String sql, Map<String, ?> paramMap)
// 分頁
find(Page<T> page, String hql, Map<String, ?> parameters)
// 分頁
find(Page<T> page, String hql, Object... parameters)
redis
Redis 是效能極佳key-value資料庫,常用來做快取,
java 中常用的客戶端有 Jedis
和 Lettuce
, spring data redis
是基於 Lettuce
做的二次封裝
maven 依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
為了在redis讀起來更方便,更改序列化方式
@Configuration
public class RedisConfig {
/**
* 設定序列化方式
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer());
return redisTemplate;
}
@Bean
public Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() {
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 將類名稱序列化到json串中
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer<Object>(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(om);
return jackson2JsonRedisSerializer;
}
}
spring cache
spring cache 抽象出了一組快取介面,通過註解的方式使用,可以很方便的配置其具體實現,詳細配置
這裡使用redis做為快取 provider, 預設的value序列化方式是JDK,為方便檢視,可以修改為用json序列化
有時會有設定redis key
字首的需求,預設是這樣的
static CacheKeyPrefix simple() {
// 在 cacheName 後面新增 "::"
return name -> name + "::";
}
spring boot 提供的有配置字首的屬性
spring.cache.redis.key-prefix= # Key prefix.
但這是一個坑,這樣寫的效果實際這樣的,會把cacheName
幹掉,顯然不是我們想要的
CacheKeyPrefix cacheKeyPrefix = (cacheName) -> prefix;
我們想要的是把字首加在最前面,保留cacheName
CacheKeyPrefix cacheKeyPrefix = (cacheName) -> keyPrefix + "::" + cacheName + "::";
參考org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration
,宣告 RedisCacheManager
@Configuration
@EnableConfigurationProperties(CacheProperties.class)
@EnableCaching
public class SpringCacheConfig {
@Autowired
private CacheProperties cacheProperties;
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheManagerBuilder builder = RedisCacheManager
.builder(redisConnectionFactory)
.cacheDefaults(determineConfiguration());
List<String> cacheNames = this.cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
}
return builder.build();
}
private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration() {
Redis redisProperties = this.cacheProperties.getRedis();
org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
.defaultCacheConfig();
// 修改序列化為json
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(jackson2JsonRedisSerializer()));
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
// 重寫字首拼接方式
config = config.computePrefixWith((cacheName) -> redisProperties.getKeyPrefix() + "::" + cacheName + "::");
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
// 省略 jackson2JsonRedisSerializer()
}
mogodb
MongoDB 是文件型資料庫,使用 spring data mogodb
可以很方便對mogodb進行操作
maven 依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
sprign data mogodb
提供了 MongoTemplate
對mogodb進行操作,我在該類的基礎上又擴充套件了一下,可以自定義自己的方法
@Configuration
public class MongoDbConfig {
/**
* 擴充套件自己的mogoTemplate
*/
@Bean
public MyMongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory,
MongoConverter converter) {
return new MyMongoTemplate(mongoDbFactory, converter);
}
}
我擴充套件了一個分頁的方法,可以根據自己的情況擴充套件其它方法
// mogodb 分頁
public <T> Page<T> find(Page<T> page, Query query, Class<T> entityClass)
mybatis
maven 依賴
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
常用配置如下
# 配置檔案位置 classpath後面要加*,不然後面萬用字元不管用
mybatis.mapperLocations=classpath*:com/zhaoguhong/baymax/*/mapper/*Mapper.xml
# 開啟駝峰命名自動對映
mybatis.configuration.map-underscore-to-camel-case=true
dao層直接用介面,簡潔,方便
@Mapper
public interface DemoMapper {
/**
* 註解方式
*/
@Select("SELECT * FROM demo WHERE user_name = #{userName}")
List<Demo> findByUserName(@Param("userName") String userName);
/**
* xml方式
*/
List<Demo> getDemos();
}
需要注意,xml的namespace必須是mapper類的全限定名,這樣才可以建立dao介面與xml的關係
<mapper namespace="com.zhaoguhong.baymax.demo.dao.DemoMapper">
<select id="getDemos" resultType="com.zhaoguhong.baymax.demo.entity.Demo">
select * from demo
</select>
</mapper>
通用mapper
mybatis 的單表增刪改查寫起來很囉嗦,通用mapper很好的解決了這個問題
maven 依賴
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>1.2.4</version>
</dependency>
常用配置如下
# 通用mapper 多個介面時用逗號隔開
mapper.mappers=com.zhaoguhong.baymax.mybatis.MyMapper
mapper.not-empty=false
mapper.identity=MYSQL
定義自己的 MyMapper
方便擴充套件,MyMapper
介面 中封裝了通用的方法,和jpa
的BaseRepository
類似,這裡不再贅述
宣告mapper
需要加Mapper
註解,還稍顯麻煩,可以用掃描的方式
@Configuration
@tk.mybatis.spring.annotation.MapperScan(basePackages = "com.zhaoguhong.baymax.**.dao")
public class MybatisConfig {
}
使用時直接繼承MyMapper介面即可
public interface DemoMapper extends MyMapper<Demo>{
}
分頁
pagehelper是一個很好用的mybatis的分頁外掛
maven 依賴
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>
常用配置如下
#pagehelper
#指定資料庫型別
pagehelper.helperDialect=mysql
#分頁合理化引數
pagehelper.reasonable=true
使用分頁
PageHelper.startPage(1, 5);
List<Demo> demos = demoMapper.selectAll();
pagehelper 還有好多玩法,可以參考這裡
自定義分頁
pagehelper 雖然好用,但專案中有自己的分頁物件,所以單獨寫一個攔截器,把他們整合到一起
這個地方要特別注意外掛的順序不要搞錯
@Configuration
// 設定mapper掃描的包
@tk.mybatis.spring.annotation.MapperScan(basePackages = "com.zhaoguhong.baymax.**.dao")
@Slf4j
public class MybatisConfig {
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
/**
* 新增自定義的分頁外掛,pageHelper 的分頁外掛PageInterceptor是用@PostConstruct新增的,自定義的應該在其後面新增
* 真正執行時順序是反過來,先執行MyPageInterceptor,再執行 PageInterceptor
*
* 所以要保證 PageHelperAutoConfiguration 先執行
*/
@Autowired
public void addPageInterceptor(PageHelperAutoConfiguration pageHelperAutoConfiguration) {
MyPageInterceptor interceptor = new MyPageInterceptor();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
log.info("註冊自定義分頁外掛成功");
}
}
}
在使用時只需要傳入自定義的分頁物件即可
Page<Demo> page = new Page<>(1, 10);
demos = demoMapper.getDemos(page);
spring security
安全模組是專案中必不可少的一環,常用的安全框架有shiro
和spring security
,shiro相對輕量級,使用非常靈活,spring security
相對功能更完善,而且可以和spring 無縫銜接。這裡選取spring security
做為安全框架
maven 依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
繼承WebSecurityConfigurerAdapter類就可以進行配置了
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 設定可以匿名訪問的url
.antMatchers(securityProperties.getAnonymousArray()).permitAll()
// 其它所有請求都要認證
.anyRequest().authenticated()
.and()
.formLogin()
// 自定義登入頁
.loginPage(securityProperties.getLoginPage())
// 自定義登入請求路徑
.loginProcessingUrl(securityProperties.getLoginProcessingUrl())
.permitAll()
.and()
.logout()
.permitAll();
// 禁用CSRF
http.csrf().disable();
}
@Override
public void configure(WebSecurity web) throws Exception {
String[] ignoringArray = securityProperties.getIgnoringArray();
// 忽略的資源,直接跳過spring security許可權校驗
if (ArrayUtils.isNotEmpty(ignoringArray)) {
web.ignoring().antMatchers(ignoringArray);
}
}
/**
*
* 宣告密碼加密方式
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.userDetailsService(userDetailsService)
// 配置密碼加密方式,也可以不指定,預設就是BCryptPasswordEncoder
.passwordEncoder(passwordEncoder());
}
}
實現UserDetailsService
介面,定義自己的UserDetailsService
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsernameAndIsDeleted(username, SystemConstants.UN_DELETED);
if (user == null) {
throw new UsernameNotFoundException("username Not Found");
}
return user;
}
}
配置項
#匿名訪問的url,多個用逗號分隔
security.anonymous=/test
#忽略的資源,直接跳過spring security許可權校驗,一般是用做靜態資源,多個用逗號分隔
security.ignoring=/static/**,/images/**
#自定義登入頁面
security.loginPage=/login.html
#自定義登入請求路徑
security.loginProcessingUrl=/login
專案上下文
為了方面使用,封裝一個上下文物件 ContextHolder
// 獲取當前執行緒HttpServletRequest
getRequest()
// 獲取當前執行緒HttpServletResponse
getResponse()
// 獲取當前HttpSession
getHttpSession()
setSessionAttribute(String key, Serializable entity)
getSessionAttribute(String key)
setRequestAttribute(String key, Object entity)
getRequestAttribute(String key)
// 獲取 ApplicationContext
getApplicationContext()
//根據beanId獲取spring bean
getBean(String beanId)
// 獲取當前登入使用者
getLoginUser()
// 獲取當前登入使用者 id
getLoginUserId()
// 獲取當前登入使用者 為空則丟擲異常
getRequiredLoginUser()
// 獲取當前登入使用者id, 為空則丟擲異常
getRequiredLoginUserId()
單點登入
單點登入系統(SSO,single sign-on)指的的,多個系統,共用一套使用者體系,只要登入其中一個系統,訪問其他系統不需要重新登入
CAS
CAS(Central Authentication Service)是耶魯大學的一個開源專案,是比較流行的單獨登入解決方案。在CAS中,只負責登入的系統被稱為服務端,其它所有系統被稱為客戶端
登入流程
- 使用者訪問客戶端,客戶端判斷是否登入,如果沒有登入,重定向到服務端去登入
- 服務端登入成功,帶著ticket重定向到客戶端
- 客戶端拿著ticket傳送請求到服務端換取使用者資訊,獲取到後就表示登入成功
登出流程
跳轉到sso認證中心進行統一登出,cas 會通知所有客戶端進行登出
spring security 整合 cas
maven 依賴
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
</dependency>
spring security 對 cas 做了很好的封裝,在使用的過程中,只需要定義好對應的登入fifter和登出fifter即可,整合cas的程式碼我寫在了WebSecurityConfig
類中
相關屬性配置
#是否開啟單點登入
cas.enable = true
#服務端地址
cas.serverUrl=
#客戶端地址
cas.clientUrl=
#登入地址
cas.loginUrl=${cas.serverUrl}/login
#服務端登出地址
cas.serverLogoutUrl=${cas.serverUrl}/logout
#單點登入成功回撥地址
cas.clientCasUrl=${cas.clientUrl}/login/cas
郵件
因為要使用freeMarker解析模板,所以也要引入freeMarker依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
相關配置
#郵件
#設定郵箱主機,163郵箱為smtp.163.com,qq為smtp.qq.com
spring.mail.host = smtp.163.com
spring.mail.username =
#授權碼
spring.mail.password =
#預設的郵件傳送人
mail.sender =
封裝一個 MailService
// 根據相關配置傳送郵件
void sendMail(MailModel mailModel);
// 傳送簡單的郵件
void sendSimleMail(String to, String subject, String content);
// 傳送html格式的郵件
void sendHtmlMail(String to, String subject, String content);
// 傳送帶附件的郵件
void sendAttachmentMail(String to, String subject, String content, String path);
// 傳送帶附件的html格式郵件
void sendAttachmentHtmlMail(String to, String subject, String content, String path);
// 根據模版傳送簡單郵件
void sendMailByTemplate(String to, String subject, String templateName,
Map<String, Object> params);
}
maven
映象
設定阿里雲映象,加快下載速度
修改 setting.xml,在 mirrors 節點上,新增
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
也可以在專案 pom.xml 檔案新增 ,僅當前專案有效
<repositories>
<repository>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
<updatePolicy>daily</updatePolicy>
</releases>
<snapshots>
<enabled>false</enabled>
<checksumPolicy>warn</checksumPolicy>
</snapshots>
<layout>default</layout>
</repository>
</repositories>
總結
- spring boot 遵循開箱即用的原則,並不需要做過多配置,網上的教程質量參差不齊,並且1.X和2.X使用時還有諸多不同,因此在使用時儘量參考官方文件
- 有時候預設的配置並不能滿足我們的需求,需要做一些自定義配置,推薦先看一下
springboot
自動配置的原始碼,再做定製化處理 - 技術沒有銀彈,在做技術選型時不要過於迷信一種技術,適合自己的業務的技術才是最好的