前言
本文主要用来演示SpringSecurity.
准备工作
一.我们需要创建一个基于SpringBoot的Maven项目 并且 配置依赖项
新建项目
配置pom依赖
<parent> 是父依赖 放在pom文件靠上的地方 <dependencies> 放在<parent> 下面
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.3.1-2</version>
</dependency>
<!-- <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.35</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--fastjson依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
</dependencies>
二.创建我们需要的目录结构
三.配置数据库连接池以及mabatis-plus参数
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/blog
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
四.配置工具类(通用 不必过多纠结)
4.1 配置FastJsonRedisSerializer
/**
* Redis使用FastJson序列化
*
* @author sg
*/
public class FastJsonRedisSerializer<T> implements RedisSerializer<T>
{
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class<T> clazz;
static
{
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
}
public FastJsonRedisSerializer(Class<T> clazz)
{
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException
{
if (t == null)
{
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException
{
if (bytes == null || bytes.length <= 0)
{
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz);
}
protected JavaType getJavaType(Class<?> clazz)
{
return TypeFactory.defaultInstance().constructType(clazz);
}
}
4.2 配置JwtUtil 加密用的
后面使用向redis 中 存储数据的时候,我们需要对key进行jwt加密.
public class JwtUtil {
public static final Long JWT_TTL = 60 * 60 *1000L;
public static final String JWT_KEY = "xms";
public static String getUUID(){
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
}
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
return builder.compact();
}
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if(ttlMillis==null){
ttlMillis=JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid)
.setSubject(subject)
.setIssuer("sg")
.setIssuedAt(now)
.signWith(signatureAlgorithm, secretKey)
.setExpiration(expDate);
}
public static String createJWT(String id, String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);
return builder.compact();
}
public static void main(String[] args) throws Exception {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg";
Claims claims = parseJWT(token);
System.out.println(claims);
}
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
4.3 配置RedisCache
这是redis 数据库的配置
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
@Autowired
public RedisTemplate redisTemplate;
public <T> void setCacheObject(final String key, final T value)
{
redisTemplate.opsForValue().set(key, value);
}
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
{
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
public boolean expire(final String key, final long timeout)
{
return expire(key, timeout, TimeUnit.SECONDS);
}
public boolean expire(final String key, final long timeout, final TimeUnit unit)
{
return redisTemplate.expire(key, timeout, unit);
}
public <T> T getCacheObject(final String key)
{
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
public boolean deleteObject(final String key)
{
return redisTemplate.delete(key);
}
public long deleteObject(final Collection collection)
{
return redisTemplate.delete(collection);
}
public <T> long setCacheList(final String key, final List<T> dataList)
{
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
public <T> List<T> getCacheList(final String key)
{
return redisTemplate.opsForList().range(key, 0, -1);
}
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
{
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext())
{
setOperation.add(it.next());
}
return setOperation;
}
public <T> Set<T> getCacheSet(final String key)
{
return redisTemplate.opsForSet().members(key);
}
public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
{
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
public <T> Map<String, T> getCacheMap(final String key)
{
return redisTemplate.opsForHash().entries(key);
}
public <T> void setCacheMapValue(final String key, final String hKey, final T value)
{
redisTemplate.opsForHash().put(key, hKey, value);
}
public <T> T getCacheMapValue(final String key, final String hKey)
{
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
public void delCacheMapValue(final String key, final String hkey)
{
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.delete(key, hkey);
}
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
{
return redisTemplate.opsForHash().multiGet(key, hKeys);
}
public Collection<String> keys(final String pattern)
{
return redisTemplate.keys(pattern);
}
}
五.配置Config 以及 Common
5.1 配置RedisConfig 通用
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
{
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
5.2 配置SecurityConfig
对于需要放行的页面 我们需要单独配置一下 比如 login 登录页面 register注册页面
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
// @Bean
// public PasswordEncoder getEncoder(){
// return new BCryptPasswordEncoder();
// }
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/user/login").anonymous()
.antMatchers("/swagger-ui.html").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
public AuthenticationManager getManager() throws Exception {
return super.authenticationManager();
}
}
5.3 配置CommonResult
package com.xms.boots.Common;
import lombok.Data;
import org.springframework.stereotype.Component;
@Component
@Data
public class CommonResult {
private int code;
private String msg;
private Object data;
}
正式代码
一:配置实体类 Users
@TableId(type = IdType.AUTO) 这句话是必须得 如果不加这句话,会找不到主键
@Data @AllArgsConstructor @NoArgsConstructor 是lombok语法
implements Serializable 让实体类 实现序列化 如果用户信息的实体类没有实现序列化接口,那么就会出现序列化异常,从而导致程序出错. 序列化是为了更好的存储数据.
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Users implements Serializable {
@TableId(type = IdType.AUTO)
private int uid;
private String username;
private String password;
private String telephone;
}
二:配置实体类 MyUserDetail
在实体类中,所有的boolean 判断 都需要更改为true 否则 会阻挡所有页面的访问
getpassword getusername 让它返回我们自己数据库中的内容
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MyUserDetail implements UserDetails {
@Autowired
private Users users;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return users.getPassword();
}
@Override
public String getUsername() {
return users.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
三:配置Mapper接口
extends BaseMapper<Users> 继承BaseMapper 是为了能够在Service中 利用mabatis-plus 更加简单的书写相关Sql语句 泛型为Users 是为了正确映射实体类的属性 更加准确的对数据进行crud
@org.apache.ibatis.annotations.Mapper
public interface UserMapper extends BaseMapper<Users> {
}
四:配置MyUserDetailServiceImpl
UserDetailService是一个接口 当我们继承这个接口的时候 我们可以重写其中的loadUserByUsername方法 我们可以通过mybatis-plus根据username 去数据库查询是否是对应的对象 ,如果没有,则直接抛出异常,告知security.如果有,则把查询到的对象封装为userDetail对象 返回Spring;
package com.xms.boots.Service.Impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xms.boots.Mapper.UserMapper;
import com.xms.boots.pojo.MyUserDetail;
import com.xms.boots.pojo.Users;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class MyUserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper wrapper = new QueryWrapper();
wrapper.eq("username",username);
Users users = userMapper.selectOne(wrapper);
if (users == null){
throw new UsernameNotFoundException("用户不存在");
}else{
MyUserDetail userDetail = new MyUserDetail(users);
return userDetail;
}
}
}
五:配置loginService 和 loginServiceImpl
public interface loginService {
CommonResult login(Users users);
}
@Service
public class loginServiceImpl implements loginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private CommonResult commonResult;
@Autowired
private RedisCache redisCache;
@Override
public CommonResult login(Users users) {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(users.getUsername(),users.getPassword());
Authentication obj = authenticationManager.authenticate(authenticationToken);
if (obj==null){
throw new RuntimeException("密码错误");
}else{
MyUserDetail detail = (MyUserDetail) obj.getPrincipal();
Users user = detail.getUsers();
String jwt = JwtUtil.createJWT(user.getUid()+"");
commonResult.setCode(200);
commonResult.setMsg("验证成功");
HashMap map = new HashMap();
map.put("token",jwt);
commonResult.setData(map);
redisCache.setCacheObject(user.getUid()+"",detail);
}
return commonResult;
}
}
六: 配置JwtAuthenticationTokenFilter
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain Chain) throws ServletException, IOException {
String token = request.getHeader("token");
if (token == null || "".equals(token)){
// throw new RuntimeException("非法访问,请先登录");
Chain.doFilter(request,response);
return;
}
String uid;
// 解析token
try{
Claims claims = JwtUtil.parseJWT(token);
uid = claims.getSubject();
}catch (Exception e){
e.printStackTrace();
throw new RuntimeException("token非法");
}
// String rediskey = "uid:" + uid;
MyUserDetail detail = redisCache.getCacheObject(uid);
if (detail == null){
throw new RuntimeException("请重新登录");
}else{
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(detail,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
Chain.doFilter(request,response);
}
}
}
七:配置 UserController
@Controller
@Data
public class UserController {
@Autowired
private loginService loginService;
@RequestMapping("/user/login")
@ResponseBody
public CommonResult login(Users users){
return loginService.login(users);
}
}